<?php
require dirname(__FILE__).DIRECTORY_SEPARATOR.'DependencyNode.php';
require dirname(__FILE__).DIRECTORY_SEPARATOR.'CyclicDependencyException.php';
require dirname(__FILE__).DIRECTORY_SEPARATOR.'UnresolvedDependencyException.php';

/**
 * This is the IoC container
 * 
 * @category	System
 * @package 	Onecollab.System
 * @author 		Mark Tolmacs <mark.tolmacs@oghma.hu>
 * @copyright 	Redmark Kft, 2008
 * @version 	$Id$
 */
class IoC_Container {
	protected static $instances;
	protected static $config;
	
	/**
	 * Sets the IoC configuration.
	 *
	 * @param array $config
	 */
	public static function setConfig($config) {
		$this->config = $config;
	}
	
	/**
	 * Instantiate a class with constructor parameter dependencies
	 * resolved and initialized.
	 *
	 * @param string $className The class name
	 * @param boolean $singleton Whether to retrieve a singleton instance
	 * @return object The instantiated and initialized object
	 */
	public static function & getInstance($className, $singleton = TRUE) {
		if ($singleton) {
			// Use singleton cache parameter to retrieve
			if (!isset(self::$instances[$className])) {
				self::$instances[$className] = self::_resolve($className, new IoC_DependencyNode($className));				
			}
			
			return self::$instances[$className];
		}
		
		return self::_resolve($className, new IoC_DependencyNode($className));				
	}
	
	/**
	 * Resolve dependencies
	 *
	 * @param string $className The class name to instantiate
	 * @return object The instantiated and initialized object
	 */
	protected static function & _resolve($className, &$node) {		
		$class = self::_getImplementation(new ReflectionClass($className));
		$implementationClassName = $class->getName();
		
		$constructor = $class->getConstructor();
		if (!is_null($constructor)) {
			$parameters = $constructor->getParameters();	
		}
		else {
			// No constructor defined so no constructor dependencies
			return self::_getInstance($class);
		}
		
		if ($constructor->getNumberOfParameters() === 0) {
			// No constructor dependencies
			return self::_getInstance($class);
		}
		else {
			$dependencies = array();
			foreach ($parameters as $parameter) {
				$dependency = $parameter->getClass();
				if ($dependency instanceof ReflectionClass) {					
					$childNode = $node->addChild($dependency->getName());
					if (self::_checkCyclicDependency($node, $childNode)) {
						throw new IoC_CyclicDependencyException('Cyclic dependency ('.$dependency->getName().') found while trying to resolve dependencies for '.$className.' ['.$implementationClassName.']');
					}
					
					$dependencies[$parameter->getName()] = self::_resolve($dependency->getName(), $childNode);					
				}				 
			}
			
			if (count($dependencies)) {
				return self::_getInstance($class, $dependencies);
			}
			else {
				return self::_getInstance($class);
			}			
		}		
		
		return FALSE;
	}
	
	/**
	 * Checks for cyclic dependencies
	 *
	 * @param string $className
	 * @param string $dependency	 
	 * @return boolean TRUE if cyclic dependency detected
	 */
	protected static function _checkCyclicDependency($thisNode, $childNode) {
		if ($thisNode->getId() === $childNode->getId()) {
			// Trivial case: A needs A
			return TRUE;
		}
		
		$node = $childNode->getParent();
		while (!is_null($node)) {
			if ($node->getId() === $childNode->getId()) {
				return TRUE;
			}
			$node = $node->getParent();
		}
		
		return FALSE;
	}
	
	/**
	 * Creates the class implementation instance. 
	 *
	 * @param ReflectionClass $class
	 * @param array $params Optional array containing the runtime parameters for the instantiation
	 * @return object The instance created
	 */
	protected static function _getInstance(ReflectionClass &$class, &$params = NULL) {
		if ($class->isInstantiable() && $class->getConstructor()->getNumberOfParameters() === 0) {
			return $class->newInstance();
		}
		else {
			if (isset($this->config['IoC']['bindings'][$class->getName()]['args'])) {
				$args = $this->config['IoC']['bindings'][$class->getName()]['args'];
				if (!is_null($params)) {
					$args = array_merge($args, $params);
				}				
				return $class->newInstanceArgs($args);	// Instantiate with the configured arguments
			}
			else if (!is_null($params)) {
				return $class->newInstanceArgs($params);
			}
			else {
				return $class->newInstance();	// Use the constructor default values
			}
							
		}
	}
	
	/**
	 * Returns the actual implementation class of the given parameter
	 *
	 * @param ReflectionClass $class
	 * @return ReflectionClass The real implementor of the provided class
	 */
	protected static function _getImplementation(ReflectionClass $class) {		
		if ($class->isInstantiable()) {
			return $class;
		}
		else {
			if (isset($this->config['IoC']['bindings'][$class->getName()])) {				
				if (isset($this->config['IoC']['bindings'][$class->getName()]['name'])) {
					return new ReflectionClass($this->config['IoC']['bindings'][$class->getName()]['name']);
				}
				else {
					return $class;
				}				
			}
			else {
				throw new IoC_UnresolvedDependencyException('An unresolvable dependency found: '.$class->getName());
			}
		}
	}
}